using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using DanielSig;
using DanielSig.UnityExtensions;

public class MasterAgent : MonoBehaviour, IAgent
{
	internal enum PathNodeType
	{
		Waypoint,
		HealthPack,
		AmmoPack
	}
	private class PathNode
	{	
		public static LayerMask Mask;
		private static int _notVisitedID;
		
		public Vector3 Position;
		public PathNodeType Type;
		
		public LinkedList<Tuple<PathNode,float>> Paths = new LinkedList<Tuple<PathNode, float>>();
		private int _visitedFlag;
		
		public static void BeginNewSearch()
		{
			unchecked
			{
				_notVisitedID++;
			}
		}
		public bool Unvisited
		{
			get
			{
				bool visited = _visitedFlag != _notVisitedID;
				_visitedFlag = _notVisitedID;
				return visited;
			}
		}
		
		public PathNode(Vector3 position)
		{
			Position = position;
			Type = PathNodeType.Waypoint;
		}
		public PathNode(Vector3 position, PathNodeType type)
		{
			Position = position;
			Type = type;
		}
		
		public void ConnectTo(PathNode other)
		{
			float dist = LineOfSight(Position, other.Position);
			if(dist >= 0)
			{
				Paths.AddLast(new Tuple<PathNode, float>(other, dist));
				other.Paths.AddLast(new Tuple<PathNode, float>(this, dist));
			}
		}
		public void ConnectTo(LinkedList<PathNode> others)
		{
			LinkedListNode<PathNode> other = others.First;
			while(other != null)
			{
				if(this != other.Value)
					ConnectTo(other.Value);
				other = other.Next;
			}
		}
		public void DisconnectAll()
		{
			foreach (Tuple<PathNode, float> path in Paths)
			{
				path.First.Paths.Remove(p => p.First == this);
			}
			Paths = null;
		}
		public static void ConnectAll(LinkedList<PathNode> nodes)
		{
			LinkedListNode<PathNode> n = nodes.First;
			while(n != null)
			{
				LinkedListNode<PathNode> other = n.Next;
				while(other != null)
				{
					n.Value.ConnectTo(other.Value);
					other = other.Next;
				}
				n = n.Next;
			}
		}
		public static LinkedList<Vector3> GetFromTo(LinkedList<PathNode> nodes, Vector3 from, Vector3 to, Statistics stats)
		{
			if(from == to)
				return null;
			
			LinkedList<Vector3> path;
			
			bool avoidEnemy = !ShouldSeekEnemy(stats);
			
			//if no line of sight of enemy, or should avoid enemy
			if(avoidEnemy || LineOfSight(from, to) < 0)
			{
				BeginNewSearch();
				
				PathFinder bestEscape = null;
				float bestEscapeValue = float.NegativeInfinity;
				Vector3 randomPos = new Vector3(Random.value * 15, 0F, Random.value * 15);
				
				LinkedList<PathFinder> candidates = PathFinder.GetCandidates(nodes, from, to, stats);
				while(candidates.Count > 0)
				{
					PathFinder p = candidates.First.Value;
					candidates.RemoveFirst();
					
					float dist = p.Node.Position.DistanceTo(randomPos);
					if(dist > bestEscapeValue)
					{
						bestEscapeValue = dist * 1.5F;
						bestEscape = p;
					}
					
					//if should avoid enemy
					if(avoidEnemy)
					{
						if(p.Node.Type != PathNodeType.Waypoint)
							return p.Path;
						else
						{
							p.Expand(candidates, to, stats);
							if(candidates.Count == 0 && bestEscape != null)
								return bestEscape.Path;
						}
					}
					else if(LineOfSight(p.Node.Position, to) < 0)//else if NOT in light of sight of enemy
					{
						p.Expand(candidates, to, stats);
					}
					else//in line of sight
					{
						path = p.Path;
						path.AddLast(to);
						return path;
					}
				}
			}
			path = new LinkedList<Vector3>();
			path.AddLast(to);
			return path;
		}
		public static float LineOfSight(Vector3 from, Vector3 to)
		{
			Ray ray = new Ray(from, from.DirectionTowards(to));
			float dist = from.DistanceTo(to);
			if(Physics.SphereCast(ray, 0.4F, dist, Mask.value))
				return -1;
			return dist;
		}
		
		//heuristics
		//only computed once for each node per search
		public float Heuristics(float cost, Vector3 to, Statistics stats)
		{
			float distToEnemy = Position.DistanceTo(to);
			float searchForAmmo = (1 - (stats.MyAmmo / (stats.EnemyHealth * 5F))).Clamp();
			float searchForHealth = (1 - (stats.EnemyAmmo / (stats.MyHealth * 30F))).Clamp();
			float isAmmo = (Type == MasterAgent.PathNodeType.AmmoPack) ? 1 : (Paths.Any(p => p.First.Type == PathNodeType.AmmoPack) ? 0.5F : 0);
			float isHealth = (Type == MasterAgent.PathNodeType.AmmoPack) ? 1 : (Paths.Any(p => p.First.Type == PathNodeType.HealthPack) ? 0.5F : 0);
			return cost
			+ distToEnemy * (1 - searchForAmmo * 2) * (1 - searchForHealth * 2)
			- isAmmo * searchForAmmo * 10000
			- isHealth * searchForHealth * 10000;
		}
		public static bool ShouldSeekEnemy(Statistics stats)
		{
			return (stats.MyAmmo > stats.EnemyHealth * 5F) && (stats.MyHealth * 30F > stats.EnemyAmmo);
		}
		
		private class PathFinder
		{
			public PathFinder Precursor;
			public PathNode Node;
			public float Cost;
			public float Value;
			
			public PathFinder(PathFinder precursor, PathNode node, float cost, Vector3 goal, Statistics stats)
			{
				Precursor = precursor;
				Node = node;
				Cost = cost;
				Value = node.Heuristics(cost, goal, stats);
			}
			
			public void Expand(LinkedList<PathFinder> candidates, Vector3 to, Statistics stats)
			{
				foreach(Tuple<PathNode, float> path in Node.Paths)
				{
					if(path.First.Unvisited)
					{
						PathFinder child = new PathFinder(this, path.First, Cost + path.Second, to, stats);
						AddSorted(candidates, child);
					}
				}
			}
			public static LinkedList<PathFinder> GetCandidates(LinkedList<PathNode> allNodes, Vector3 from, Vector3 to, Statistics stats)
			{
				LinkedList<PathFinder> candidates = new LinkedList<PathFinder>();
				foreach(PathNode node in allNodes)
				{
					//Debug.DrawLine(from + Vector3.down * 0.2F, node.Position + Vector3.down * 0.2F, (Color.red + Color.black) * 0.5F, 0.2F);
					float cost = PathNode.LineOfSight(from, node.Position);
					if(cost >= 0)
					{
						if(node.Unvisited)
						{
							PathFinder candidate = new PathFinder(null, node, cost, to, stats);
							AddSorted(candidates, candidate);
						}
					}
				}
				return candidates;
			}
			public LinkedList<Vector3> Path
			{
				get
				{
					LinkedList<Vector3> path = new LinkedList<Vector3>();
					PathFinder p = this;
					while(p != null)
					{
						path.AddFirst(p.Node.Position);
						p = p.Precursor;
					}
					return path;
				}
			}
			private static void AddSorted(LinkedList<PathFinder> candidates, PathFinder candidate)
			{
				//if(candidate.Precursor != null) Debug.DrawLine(candidate.Precursor.Node.Position, candidate.Node.Position, Color.black, 0.2F);
				candidates.AddSorted(candidate,
					p => p.Value
				);
			}
		}
	}
	
	[System.Serializable]
	internal class Statistics
	{
		public float MyHealth;
		public float EnemyHealth;
		public int MyAmmo;
		public int EnemyAmmo;
	}

	public LayerMask StaticObjectsMask;
	public string OpponentTag;
	public int PathFindingInterval = 20;
	public float MinDistanceToOpponent = 1F;
	public ProjectileController ProjectilePrefabController;
	
	private int _initialized = 0;
	private int _pathFindingCounter = 0;
	private LinkedList<PathNode> _nodes;
	private Dictionary<GameObject, LinkedListNode<PathNode>> _pickups;
	private GameObject _enemy;
	private Statistics _stats;
	private Vector3 _prevTargetPos;
	
	private LinkedList<Vector3> _path;
	
	private void Init ()
	{
		_pickups = new Dictionary<GameObject, LinkedListNode<PathNode>>();
		_stats = new Statistics();
		
		GameObject[] obstacles = GameObject.FindGameObjectsWithTag("Obstacle");
		
		_nodes = new LinkedList<PathNode>();
		HashSet<Vector2> nodePool = new HashSet<Vector2>();
		
		foreach (GameObject obstacle in obstacles)
		{
			Vector3 pos = obstacle.transform.position;
			pos.y = 0;
			
			for(int x = -1; x <= 1; x++)
			{
				for(int z = -1; z <= 1; z++)
				{
					if(x == 0 && z == 0) continue;
					
					Vector3 nodePos = pos;
					nodePos.x += x;
					nodePos.z += z;
					
					Vector2 pos2D = nodePos.XZ();
					
					if(SpawnPool.IsFreeSpace(pos2D) && !nodePool.Contains(pos2D))
					{
						//Debug.DrawRay(nodePos, Vector3.up, Color.red, 1F);
						_nodes.AddLast(new PathNode(nodePos));
						nodePool.Add(pos2D);
					}
				}
			}
		}
		for(int i = 0; i < 4; i++)
		{
			Vector3 nodePos = new Vector3((i % 2) * 15, 0, (i >> 1) * 15);
			
			Vector2 pos2D = nodePos.XZ();
			
			if(SpawnPool.IsFreeSpace(pos2D) && !nodePool.Contains(pos2D))
			{
				//Debug.DrawRay(nodePos, Vector3.up, Color.red, 1F);
				_nodes.AddLast(new PathNode(nodePos));
			}
		}
		
		PathNode.Mask = StaticObjectsMask;
		PathNode.ConnectAll(_nodes);
		
		/*foreach (PathNode n in _nodes)
		{
			Vector3 drawPos = n.Position;
			foreach (Tuple<PathNode, float> other in n.Paths)
			{
				Debug.DrawLine(drawPos, other.First.Position, Color.blue, 1F);
			}
		}*/
		
		_enemy = GameObject.FindWithTag(OpponentTag);
		_prevTargetPos = _enemy ? _enemy.transform.position : Vector3.zero;
	}
	private void UpdatePickups ()
	{
		GameObject[] healthPacks = GameObject.FindGameObjectsWithTag("Health");
		GameObject[] ammoPacks = GameObject.FindGameObjectsWithTag("Ammo");
		
		//delete old
		LinkedList<GameObject> oldPickups = null;
		foreach (KeyValuePair<GameObject, LinkedListNode<PathNode>> node in _pickups)
		{
			if(!node.Key)
			{
				node.Value.Value.DisconnectAll();
				_nodes.Remove(node.Value);
				if(oldPickups == null)
					oldPickups = new LinkedList<GameObject>();
				oldPickups.AddLast(node.Key);
			}
		}
		if(oldPickups != null)
		{
			foreach (GameObject old in oldPickups)
			{
				_pickups.Remove(old);
			}
		}
		//add new health packs
		foreach (GameObject health in healthPacks)
		{
			if(!_pickups.ContainsKey(health))
			{
				PathNode newNode = new PathNode(health.transform.position, PathNodeType.HealthPack);
				newNode.ConnectTo(_nodes);
				LinkedListNode<PathNode> link = _nodes.AddLast(newNode);
				_pickups.Add(health, link);
			}
		}
		//add new ammo packs
		foreach (GameObject ammo in ammoPacks)
		{
			if(!_pickups.ContainsKey(ammo))
			{
				PathNode newNode = new PathNode(ammo.transform.position, PathNodeType.AmmoPack);
				newNode.ConnectTo(_nodes);
				LinkedListNode<PathNode> link = _nodes.AddLast(newNode);
				_pickups.Add(ammo, link);
			}
		}
	}
	private void UpdateStatistics (ISensor[] input)
	{
		_stats.MyHealth = (from s in input where s.Type == SensationType.MyHealth select s.Values[0]).FirstOrDefault();
		_stats.EnemyHealth = (from s in input where s.Type == SensationType.EnemyHealth select s.Values[0]).FirstOrDefault();
		_stats.MyAmmo = Mathf.RoundToInt((from s in input where s.Type == SensationType.MyAmmo select s.Values[0]).FirstOrDefault());
		_stats.EnemyAmmo = Mathf.RoundToInt((from s in input where s.Type == SensationType.EnemyAmmo select s.Values[0]).FirstOrDefault());
	}
	
	public Move NextMove (ISensor[] input)
	{
		if(_initialized > 4)
		{
			Move move = Move.None;
			
			if(!_enemy) _enemy = GameObject.FindWithTag(OpponentTag);
			
			Vector3 pos = transform.position;
			Vector3 target = _enemy ? _enemy.transform.position : Vector3.zero;
			
			UpdateStatistics(input);
			//if it should avoid the enemy or if it is NOT in line of sight of the enemy
			if(!PathNode.ShouldSeekEnemy(_stats) || PathNode.LineOfSight(pos, target) < 0)
			{
				if(--_pathFindingCounter < 0)
				{
					_pathFindingCounter = PathFindingInterval;
					UpdatePickups();
					_path = PathNode.GetFromTo(_nodes, pos, target, _stats);
				}
				
				Vector3 currentTarget = _path != null && _path.First != null ? _path.First.Value : target;
				
				move |= MoveTowards(currentTarget);
				
				float dist = pos.DistanceToSqr(currentTarget);
				if(dist < 1F)
				{
					if(PathNode.LineOfSight(pos, currentTarget) >= 0)//next target is visible
					{
						if(_path != null)
							_path.RemoveFirst();
						else
							transform.position = transform.position + Vector3.right * 0.1F;
					}
				}
			}
			else//in line of sight and should seek enemy
			{
				_pathFindingCounter = 0;
				float distanceToEnemy = pos.DistanceTo(target);
				Vector3 enemyMovementForcast = ((target - _prevTargetPos) / GameCore.IterationsPerFrame) * distanceToEnemy / (ProjectilePrefabController.Speed * Time.fixedDeltaTime);
				Debug.DrawLine(pos, target + enemyMovementForcast, Color.cyan);
				if(distanceToEnemy > MinDistanceToOpponent)
					move |= MoveTowards(target + enemyMovementForcast);
				else
					move |= TurnTowards(target + enemyMovementForcast);
				if(((float)(from s in input where s.Type == SensationType.Enemy select s.Values[1]).FirstOrDefault()).IsBetween(-0.1F, 0.1F))
					move |= Move.Shoot;
			}
			if(_path != null)
				DrawPath(_path);
			
			_prevTargetPos = target;
			
			return EscapeProjectiles(move, input);
		}
		else if(_initialized++ == 4)
		{
			//initialization
			Init();
		}
		
		return Move.None;
	}
	private Move EscapeProjectiles(Move move, ISensor[] input)
	{
		var projectiles = from s in input
			where s.Type == SensationType.Projectile && s.Values.Length > 2 && s.Values[2].IsBetween(-0.2F, 0.2F)
			select s;
		//Debug.Log(projectiles.Count());
		if(projectiles.Count() > 0)
		{
			move = Move.None;
			//move &= ~(Move.TurnLeft | Move.TurnRight);//remove turning
			bool projectileGoesCW = (float)(projectiles.FirstOrDefault().Values[2]) > 0;
			bool projectileOnRight = (float)(projectiles.FirstOrDefault().Values[1]) > 0;
			if(projectileGoesCW && projectileOnRight)
				move |= Move.TurnLeft;
			else if(!projectileGoesCW)
				move |= Move.TurnRight;
			
			move |= Move.GoForward;
		}
		return move;
	}
	private Move MoveTowards(Vector3 target)
	{
		Move move = Move.None;
		
		Vector3 direction = transform.position.DirectionTowards(target);
		float dir = transform.right.Dot(direction);
		if(dir > 0)
		{
			move |= Move.TurnRight;
		}
		else
		{
			move |= Move.TurnLeft;
		}
		if(dir.IsBetween(-0.2F, 0.2F))
		{
			move |= Move.GoForward;
		}
		return move;
	}
	private Move TurnTowards(Vector3 target)
	{
		Move move = Move.None;
		Vector3 direction = transform.position.DirectionTowards(target);
		float dir = transform.right.Dot(direction);
		if(dir > 0)
		{
			move |= Move.TurnRight;
		}
		else
		{
			move |= Move.TurnLeft;
		}
		return move;
	}
	private void DrawPath(LinkedList<Vector3> path)
	{
		Vector3 prev = transform.position;
		foreach(Vector3 point in path)
		{
			Debug.DrawLine(prev, point, Color.red);
			prev = point;
		}
	}
	void OnDrawGizmosSelected()
	{
		if(!Application.isPlaying) return;
		
		Gizmos.color = Color.white.SetAlpha(0.25F);
		foreach(PathNode node in _nodes)
			foreach(Tuple<PathNode, float> other in node.Paths)
				Gizmos.DrawLine(node.Position, other.First.Position);
	}
	/*
	private Move WhatToDoWhenNoEnemy = Move.None;
	
	private int _evadeCountdown = 0;
	private Vector3 _prevPos = -Vector3.one;
	
	public Move NextMove (ISensor[] input)
	{
		Move move = Move.None;
		
		ISensor myAmmo = input.FirstOrDefault(s => s.Type == SensationType.MyAmmo);
		ISensor myHealth = input.FirstOrDefault(s => s.Type == SensationType.MyHealth);
		
		SensationType targetType = myAmmo.Values[0] > 0 ? SensationType.Enemy : SensationType.AmmoPack;
		targetType = myHealth.Values[0] > 0.5F ? targetType : SensationType.HealthPack;
		
		ISensor target = input.FirstOrDefault(s => s.Type == targetType);
		
		if(target == null)
		{
			if(targetType == SensationType.HealthPack)
				targetType = myAmmo.Values[0] > 0 ? SensationType.Enemy : SensationType.AmmoPack;
			else targetType = SensationType.Enemy;
			
			target = input.FirstOrDefault(s => s.Type == targetType);
			
			if(target == null)
				target = input.FirstOrDefault(s => s.Type == SensationType.Enemy);
		}
		
		ISensor obstacle = input.FirstOrDefault(
			s =>
			{
				if(s.Type == SensationType.Obstacle)
				{
					float x = s.Values[1];
					float z = s.Values[0];
					return x.IsBetween(-0.5F, 0.5F) && z < 1;
				}
				return false;
			}
		);
		
		if(target != null)
		{
			WhatToDoWhenNoEnemy = Move.None;
			
			float dist = target.Values[0];
			float dir = target.Values[1];
			
			if(obstacle != null)
			{
				dir = -obstacle.Values[1];
				_evadeCountdown = 5;
			}
			else if(_evadeCountdown > 0) _evadeCountdown--;
			
			if(obstacle != null || _evadeCountdown == 0)
			{
				if(dir < 0)
					move |= Move.TurnLeft;
				if(dir > 0)
					move |= Move.TurnRight;
			}
				
			if(dir.IsBetween(-0.3, 0.3) && dist > (targetType == SensationType.Enemy ? 2 : 0) && obstacle == null || _evadeCountdown > 0)
				move |= Move.GoForward;
			
			if(targetType == SensationType.Enemy && dir.IsBetween(-0.1F, 0.1F))
			{
				move |= Move.Shoot;
			}
		}
		else
		{
			if(WhatToDoWhenNoEnemy == Move.None)
				WhatToDoWhenNoEnemy = Random.value > 0.5F ? Move.TurnLeft : Move.TurnRight;
			move |= WhatToDoWhenNoEnemy;
		}
		
		Vector3 pos = transform.position;
		
		if(pos == _prevPos || move == Move.None || ((move & (Move.TurnLeft | Move.TurnRight)) == (Move.TurnLeft | Move.TurnRight) ))
		{
			move = Move.GoForward;
			move |= Random.value < 0.5F ? Move.TurnLeft : Move.TurnRight;
		}
		_prevPos = pos;
		
		return move;
	}
	*/
}

